Skip to content

Increase expected size of output sample buffer, to correctly handle input termination. - #22

Open
arlofaria-cartesia wants to merge 2 commits into
tuxu:masterfrom
arlofaria-cartesia:fix-new-size
Open

arlofaria-cartesia wants to merge 2 commits into
tuxu:masterfrom
arlofaria-cartesia:fix-new-size

Conversation

@arlofaria-cartesia

Copy link
Copy Markdown

Problem: when using the full API (.process() method in this wrapper), the number of output samples is incorrect for a chunked input at the end of a stream. This is because the calculation of new_size assumes that the number of output samples that will be generated will be at most the number of input samples times the resampling ratio. While that's generally true for chunks processed mid-stream (as well as at the start of a stream), when the input is terminated it becomes possible for the output samples to exceed this expected length because the resampler's buffer is being flushed of any remaining data.

Solution: an ideal solution would be to determine the actual number of samples that will be flushed on an input-terminating call. This is complicated to do, for various reasons related to issues such as libsndfile/libsamplerate#6

As a hacky workaround, we can figure that adding a "fudge factor" of 10,000 samples ought to be sufficient. In case that's not enough, a RuntimeError will be raised.

Testing: Here's a modified version of the example code on the README ... perhaps this chunked usage should be included in the documentation?

#!/usr/bin/env python3

import numpy as np
import samplerate

# Synthesize data
fs = 1000.
t = np.arange(fs * 2) / fs
input_data = np.sin(2 * np.pi * 5 * t)

# Simple API
ratio = 1.5
converter = 'sinc_best'  # or 'sinc_fastest', ...
output_data_simple = samplerate.resample(input_data, ratio, converter)

# Full API
resampler = samplerate.Resampler(converter, channels=1)
output_data_full = resampler.process(input_data, ratio, end_of_input=True)

# The result is the same for both APIs.
assert np.allclose(output_data_simple, output_data_full)

# Full API, chunked
resampler.reset()
output_data_chunked = np.array([], dtype=np.float32)
chunk_size = 100
for i in range(0, len(input_data), chunk_size):
    chunk = input_data[i : i+chunk_size]
    resampled = resampler.process(chunk, ratio, end_of_input=False)
    print(f"{len(chunk)} input samples --> {len(resampled)} output samples")
    output_data_chunked = np.concatenate((output_data_chunked, resampled))
print(f"{len(output_data_chunked)} output samples before input is terminated")

# Terminate with an empty final chunk.
resampled = resampler.process([], ratio, end_of_input=True)
print(f"input terminated --> {len(resampled)} output samples")
output_data_chunked = np.concatenate((output_data_chunked, resampled))
print(f"{len(output_data_chunked)} output samples after input is terminated")

# The result is the same for all APIs.
assert np.allclose(output_data_chunked, output_data_simple)

# See `samplerate.CallbackResampler` for the Callback API, or
# `examples/play_modulation.py` for an example.

shauneccles referenced this pull request in LedFx/samplerate-ledfx Sep 27, 2026
… use

Output size: a src_process() call can generate more than
ceil(input_frames * ratio). With end_of_input the converter flushes the
input it held back (about ratio x its filter half-length: 144 input
frames for sinc_best, so ~37k output frames at ratio 256), and after a
ratio change libsamplerate ramps from the previous ratio, which has no
fixed bound. Today process() silently drops that output, and a fixed
extra allowance (#22) only moves the failure. Resampler.process() now
keeps calling src_process() with a larger buffer until libsamplerate
stops short of filling it. resample() uses the same path, since
src_simple() is src_new() + one src_process() + src_delete() and cannot
continue.

Callback: hold the GIL for the whole of the_callback_func, since the
buffer_info destructor releases a Python buffer, and store any exception
(ours or one raised by the Python callback) instead of letting it unwind
through libsamplerate's C frames; read() re-raises it.

Also run the asyncio tests in CI (pytest-asyncio).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant